β

Как стать автором
Курсы по фронтенду
Какой вы ML-специалист: составим портреты типичных ML’щиков

Моя лентаВсе потоки

marshinov 18 сен 2017 в 00:31

«Паттерны» функционального программирования

10 мин

74K

.NET*C#*Анализ и проектирование систем*Проектирование и рефакторинг*Функциональное программирование*

Туториал

Перевод

Автор оригинала: Scott Wlashin

Многие люди представляют функциональное программирование как нечто очень сложное и «наукоемкое», а представителей ФП-сообщества – эстетствующими философами, живущими в башне из слоновой кости.

До недавнего времени такой взгляд на вещи действительно был недалек от истины: говорим ФП, подразумеваем Хаскель и теорию категорий. В последнее время ситуация изменилась и функциональная парадигма набирает обороты в web-разработке, не без помощи F#, Scala и React. Попробуем взглянуть на «паттерны» функционального программирования, полезные для решения повседневных задач с точки зрения ООП – парадигмы.

ООП широко распространено в разработке прикладного ПО не одно десятилетие. Все мы знакомы с SOLID и GOF. Что будет их функциональным эквивалентом?.. Функции! Функциональное программирование просто «другое» и предлагает другие решения.

Основные принципы функционального проектирования (дизайна)

Функции как объекты первого класса

В отличие от «классического» ООП (первые версии C++, C#, Java) функции в ФП представляют собой самостоятельные объекты и не должны принадлежать какому-либо классу. Удобно представлять функцию как волшебный железнодорожный тоннель: подаете на вход яблоки, а на выходе получаете бананы (apple -> banana).

Синтаксис F# подчеркивает, что функции и значения равны в правах:

let z = 1
let add = x + y // int -> int ->int

Композиция как основной «строительный материал»

Если у нас есть две функции, одна преобразующая яблоки в бананы (apple -> banana), а другая бананы в вишни (banana -> cherry), объединив их мы получим функции преобразования яблок в вишни (apple -> cherry). С точки зрения программиста нет разницы получена эта функция с помощью композиции или написана вручную, главное – ее сигнатура.

Композиция применима как на уровне совсем небольших функций, так и на уровне целого приложения. Вы можете представить бизнес-процесс, как цепочку вариантов использования (use case) и скомпоновать их в функцию httpRequest -> httpResponse. Конечно это возможно только для синхронных операций, но для асинхронных есть реактивное функциональное программирование, позволяющее сделать тоже самое.

Можно представлять себе композицию функций как фрактал. Определение фрактала в строгом смысле не совпадает с определением композиции. Представляя фрактал вы можете визуализировать как ваш control flow состоит из скомпонованных функций, состоящих из скомпонованных функций, состоящих из…

Шаблон компоновщик (Composite) в ООП тоже можно представлять себе «фракталом», но компоновщик работает со структурами данных, а не преобразованиями.

Типы != классы

У системы типов в ФП больше общего с теорией множеств, чем с классами из ООП. int – это тип. Но тип не обязательно должен быть примитивом. Customer – это тоже тип. Функции могут принимать на вход и возвращать функции. int -> int – тоже тип. Так что «тип» — это название для некоторого множества.

Типы тоже можно компоновать. Большая часть функциональных ЯП работает с алгебраической системой типов, отличающейся от системы классов в ООП.

Перемножение (логическое «и», record type в F#)
На первый взгляд это может показаться странным, однако в этом есть смысл. Если взять множество людей и множество дат, «перемножив» их мы получим множество дней рождений.

type Birthday = Person * Date

Сложение (логическое «или», discriminated union type в F#)

type PaymentMethod =
| Cash
| Cheque of ChequeNumber
| Card of CardType * CardNumber

Discriminated union – сложное название. Проще представлять себе этот тип как выбор. Например, вы можете на выбор оплатить товар наличными, банковским переводом или с помощью кредитной карты. Между этими вариантами нет ничего общего, кроме того, все они являются способом оплаты.

Однажды нам пригодились «объединения» для моделирования предметной модели.

Entity Framework умеет работать с такими типами из коробки, нужно лишь добавить id.

Стремление к «полноте»

Давайте рассмотрим функцию «разделить 12 на». Ее сигнатура int -> int и это ложь! Если мы подадим на вход 0, функция выбросит исключение. Вместо этого мы можем заменить сигнатуру на NonZeroInteger -> int или на int -> int option.

ФП подталкивает вас к более строгому и полному описанию сигнатур функций. Если функции не выбрасывают исключений вы можете использовать сигнатуру и систему типов в качестве документации. Вы также можете использовать систему типов для создания предметной модели (Domain Model) и описания бизнес-правил (Business Rules). Таким образом можно гарантировать, что операции не допустимые в реальном мире не будут компилироваться в приложении, что дает более надежную защиту, чем модульные тесты. Подробнее об этом подходе вы можете прочитать в отдельной статье.

Функции в качестве аргументов

Хардкодить данные считается дурным тоном в программирование, вместо этого мы передаем их в качестве параметров (аргументов методов). В ФП мы идем дальше. Почему бы не параметризировать и поведение?

Вместо функции с одним аргументом опишем функцию с двумя. Теперь не важно, что это за список и куда мы выводим данные (на консоль или в лог).

let printList anAction aList =
for i in aList do
anAction i

Пойдем дальше. Рассмотрим императивный пример на C#. Очевидно, что в данном коде присутствует дублирование (одинаковые циклы). Для того чтобы устранить дублирование нужно выделить общее и выделить общее в функцию:

public static int Product(int n)
{
int product = 1; // инициализация
for (int i = 1; i <= n; i++) // цикл
{
product *= i; // действие
}

return product; // возвращаемое значение
}

public static int Sum(int n)
{
int sum = 0; // инициализация
for (int i = 1; i <= n; i++) // цикл
{
sum += i;
}

return sum; // возвращаемое значение
}

В F# для работы с последовательностями уже есть функция fold:

let product n =
let initialValue = 1
let action productSoFar x = productSoFar * x

[1..n] |> List.fold action initialValue

let sum n =
let initialValue = 0
let action sumSoFar x = sumSoFar+x

[1..n] |> List.fold action initialValue

Но, позвольте, в C# есть Aggregate, который делает тоже самое! Поздравляю, LINQ написан в функциональном стиле :)

Рекомендую цикл статей Эрика Липперта о монадах в C#. С десятой части начинается объяснение «монадической» природы SelectMany

Функции в качестве интерфейсов

Допустим у нас есть интерфейс.

interface IBunchOfStuff
{
int DoSomething(int x);
string DoSomethingElse(int x); // один интерфейс - одно дело
void DoAThirdThing(string x); // нужно разделить
}

Если взять SRP и ISP и возвести их в абсолют все интерфейсы будут содержать только одну функцию.

interface IBunchOfStuff
{
int DoSomething(int x);
}

Тогда это просто функция int -> int. В F# не нужно объявлять интерфейс, чтобы сделать функции взаимозаменяемыми, они взаимозаменяемы «из коробки» просто по своей сигнатуре. Таким образом паттерн «стратегия» реализуется простой передачей функции в качестве аргумента другой функции:

let DoSomethingWithStuff strategy x =
strategy x

Паттерн «декоратор» реализуется с помощью композиции функций

let isEvenWithLogging = log >> isEven >> log // int -> bool

Здесь автор для простоты изложения опускает вопросы семантики. При моделировании реальных предметных моделей одной сигнатуры функции не всегда достаточно.

Каррирование и частичное применение

Итак, использую одну только композицию мы можем проектировать целые приложения. Плохие новости: композиция работает только с функциями от одного параметра. Хорошие новости: в ФП все функции являются функциями от одного параметра.

Обратите внимание, сигнатура int -> int -> int не содержит скобок не случайно. Можно воспринимать сложение, как функцию от двух аргументов типа int, возвращающую значение типа int или как функцию от одного аргумента, возвращающую функциональный тип int -> int. Возвращаемая функция будет называться сумматор по основанию n, где n — число переданное аргументом в первую функцию. Повторив эту операцию рекурсивно можно функцию от любого числа аргументов преобразовать в функции от одного аргумента.

Такие преобразования возможны не только для компилируемых функций в программировании, но и для математических функций. Возможность такого преобразования впервые отмечена в трудах Готтлоба Фреге, систематически изучена Моисеем Шейнфинкелем в 1920-е годы, а наименование получило по имени Хаскелла Карри — разработчика комбинаторной логики, в которой сведение к функциям одного аргумента носит основополагающий характер.

Возможность преобразования функций от многих аргументов к функции от одного аргумента естественна для функциональных ЯП, поэтому компилятор не будет против, если вы передадите только одно значения для вычисления суммы.

let three = 1 + 2
let three = (+) 1 2
let three = ((+) 1) 2
let add1 = (+) 1
let three = add1 2

Это называется частичным применением. В функциональных ЯП частичное применение заменяет принцип инъекции зависимостей (Dependency Injection)

// эта функция требует зависимость
let getCustomerFromDatabase connection (customerId:CustomerId) =
from connection
select customer
where customerId = customerId

// а эта уже нет
let getCustomer1 = getCustomerFromDatabase myConnection

Продолжения (continuations)

Зачастую решения, закладываемые в реализацию, оказываются не достаточно гибкими. Вернемся к примеру с делением. Кто сказал, что я хочу, чтобы функция выбрасывала исключения? Может быть мне лучше подойдет «особый случай»

int Divide(int top, int bottom)
{
if (bottom == 0)
{
// кто решил, что нужно выбросить исключение?
throw new InvalidOperationException("div by 0");
}
else
{
return top/bottom;
}
}

Вместо того, чтобы решать за пользователя, мы можем предоставить решение ему:

void Divide(int top, int bottom, Action ifZero, Action<int> ifSuccess)
{
if (bottom == 0)
{
ifZero();
}
else
{
ifSuccess( top/bottom );
}
}

Если вы когда-нибудь писали асинхронный код, то наверняка знакомы с «пирамидой погибели» (Pyramid Of Doom)

Продолжения позволяют исправить этот код и избавиться от уровней вложенности. Для этого необходимо инкапуслировать условный переход в функцию:

let ifSomeDo f opt =
if opt.IsSome then
f opt.Value
else
None

И переписать код, используя продолжения

let example input =
doSomething input
|> ifSomeDo doSomethingElse
|> ifSomeDo doAThirdThing
|> ifSomeDo (fun z -> Some z)

Монады

Монады – это одно из «страшных» слов ФП. В первую очередь, из-за того, что обычно объяснения начинаются с теории категорий. Во вторую — из-за того что «монада» — это очень абстрактное понятие, не имеющее прямой аналогии с объектами реального мира. Я большой сторонник подхода «от частного к общему». Поняв практическую пользу на конкретном примере проще двигаться дальше к более полному и абстрактному определению.

Зная о «продолжениях», вернемся к аналогии с рельсами и тоннелем. Функцию, в которую передаются аргумент и два «продолжения» можно представить как развилку.

Но такие функции не компонуются :(

На помощь приходит функция bind

let bind nextFunction optionInput =
match optionInput with
// передаем результат выполнения предыдущей функции в случае успеха
| Some s -> nextFunction s
// или просто пробрасываем значение None дальше
| None -> None

Код пирамиды погибели может быть переписан с помощью bind

// было
let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
let z = doAThirdThing (y.Value)
if z.IsSome then
let result = z.Value
Some result
else
None
else
None
else
None

// стало
let bind f opt =
match opt with
| Some v -> f v
| None -> None

let example input =
doSomething input
|> bind doSomethingElse
|> bind doAThirdThing
|> bind (fun z -> Some z)

Кстати, это называется «monadic bind». Скажите своим друзьям, любителям хаскеля, что вы знаете, что такое «monadic bind» и вас примут в тайное общество:)

Bind можно использовать для сцепления асинхронных операций (промисы в JS устроены именно так)

Bind для обработки ошибок

Если у вас появилось смутное ощущение, что дальше идет описание монады Either, так оно и есть

Рассмотрим код на C#. Он выглядит достаточно хорошо: все кратко и понятно. Однако в нем отсутствует обработка ошибок. Действительно, что может пойти не так?

string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
validateRequest(request);
canonicalizeEmail(request);
db.updateDbFromRequest(request);
smtpServer.sendEmail(request.Email)
return "OK";
}

Мы все знаем, что обрабатывать ошибки нужно. Добавим обработку.

string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated)
{
return "Request is not valid"
}

canonicalizeEmail(request);
try
{
var result = db.updateDbFromRequest(request);
if (!result)
{
return "Customer record not found"
}
}
catch
{
return "DB error: Customer record not updated"
}

if (!smtpServer.sendEmail(request.Email))
{
log.Error "Customer email not sent"
}

return "OK";
}

Вместо шести понятных теперь 18 не понятных строчек. Это 200% дополнительных строчек кода. Кроме того, линейная логика метода теперь зашумлена ветвлениями и ранними выходами.

С помощью bind можно абстрагировать логику обработки ошибок. Вот так будет выглядеть метод без обработки ошибок, если его переписать на F#:

А вот этот код но уже с обработкой ошибок:

Более подробно эта тема раскрыта в отдельном докладе.

Функторы

Мне не очень понравилось описание функторов у Скотта. Прочитайте лучше статью «Функторы, аппликативные функторы и монады в картинках»

Моноиды

К сожалению, для объяснения моноидов не подходят простые аналогии. Приготовьтесь к математике.

Я предупредил, итак, математика

1 + 2 = 3

1 + (2 + 3) = (1 + 2) + 3

1 + 0 = 1

0 + 1 = 1

И еще немного

2 * 3 = 6

2 * (3 * 4) = (2 * 3) * 4

1 * 2 = 2

2 * 1 = 2

Что общего между этими примерами?

Есть некоторые объекты, в данном случае числа, и способ их взаимодействия. Причем результат взаимодействия — это тоже число (замкнутость).

Порядок взаимодействия не важен (ассоциативность).

Кроме того, есть некоторый специальный элемент, взаимодействие с которым не меняет исходный объект (нейтральный элемент).

За более строгим определением обратитесь к википедии. В рамках статьи обсуждается лишь несколько примеров применения моноидов на практике.

Замкнутость

Дает возможность перейти от попарных операций к операциям на списках

1 * 2 * 3 * 4
[ 1; 2; 3; 4 ] |> List.reduce (*)

Ассоциативность

Применение принципа «разделяй и властвуй», «халявная» параллелизация. Если у нашего процессора 2 ядра и нам нужно рассчитать значение 1 + 2 + 3 + 4. Мы можем вычислить 1 + 2 на первом ядре, а 3 + 4 — на втором, а результат сложить. Больше последовательных вычислений — больше ядер.

Нейтральный элемент

С reduce есть несколько проблем: что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент.

Кстати, в математике часто встречается определение моноида как полугруппы с нейтральным элементом. Если нейтральный элемент отсутствует, то можно попробовать его доопределить, чтобы воспользоваться преимуществами моноида.

Map / Reduce

Если ваши объекты — не моноиды, попробуйте преобразовать их. Знаменитая модель распределенных вычислений Google — не более чем эксплуатация моноидов.

Эндоморфизмы

Функции с одинаковым типом входного и выходного значения являются моноидами и имеют специальное название — «эндоморфизмы» (название заимствовано из теории категорий). Что более важно, функции, содержащие эндоморфизмы могут быть преобразованы к эндоморфизмам с помощью частичного применения.

Грег Янг открыто заявляет, что Event Sourcing — это просто функциональный код. Flux и unidirectional data flow, кстати тоже.

Монады VS моноиды

Монады являются моноидами, ведь как известно, монада — это всего лишь моноид в категории эндофункторов, а монадические законы — не более чем определение моноида в контексте продолжений.

Кстати, бастион ООП — GOF тоже содержит монады. Паттерн «интерпретатор» — это так называемая свободная монада.

Теги:

f#

ref#actoring

patterns

functional programming

Хабы:

.NET

C#

Анализ и проектирование систем

Проектирование и рефакторинг

Функциональное программирование

+51

373

361
+361

204

Карма

0

Рейтинг

Максим Аршинов @marshinov

В саббатикале

Подписаться

Комментарии 361
+361

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.6K

Кейс

Перевод

+27

19

2
+2

lokosst 12 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.3K

Мнение

+19

17

59
+59

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

29

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Показать еще

Минуточку внимания

Турбо

Делаем сисадмина сильнее с помощью менеджера паролей

Событие

Что ни день – то ивент, если следить за Календарём

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Курсы

Методы описания бизнес-процессов (IDEF, DFD, BPMN, EPC, UML)
7 октября 2025BABOKSchool

Задачи по программированию на языке C++
По желаниюStepik

Основы программирования на C. Задачи.
По желаниюStepik

Программа курсов: Бэкенд-разработка на Python
По желаниюStepik

PRO Go. Базовый курс по структурам данных
По желаниюStepik

Больше курсов на Хабр Карьере

Читают сейчас

Как превратить ноутбук в полноценное рабочее место не хуже стационарного ПК

12K

71
+71

Нашел ЛЯП в игре 5букв от Т-банка, который позволяет со 100% вероятностью угадать скрытое слово

54K

115
+115

Как выйти из IT?.. и пойти слесарем на завод. Моя попыточка дауншифтинга [4\5]

1.4K

12
+12

Моё мини-исследование про продолжительность жизни в России

6.3K

59
+59

GPT-5 получит модуль для борьбы с «галлюцинациями» — The Information

688

3
+3

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Ближайшие события

7 августа

Мем-пати: как построить IT-бренд через вирусный контент и юмор

Москва

Больше событий в календаре

Маркетинг

13 августа

Как повысить эффективность врача в стоматологии на 25% с помощью ИИ?

Онлайн

Больше событий в календаре

Менеджмент

13 августа

Тест-драйв Хабра для маркетологов и пиарщиков

Москва

Больше событий в календаре

Маркетинг

16 августа

«ИТ-летник» — конференция от Совкомбанк Технологий

Казань

Больше событий в календаре

Разработка

Аналитика

3 сентября

«GoCloud Tech 2025» — IT-конференция про облака и AI

Москва • Онлайн

Больше событий в календаре

Разработка

18 – 19 сентября

Конференция по корпоративному онлайн-обучению iSpring Days 2025

Москва • Онлайн

Больше событий в календаре

Администрирование

Менеджмент

Другое

19 сентября

Премия для технологических стартапов Startech.Awards 2025

Москва • Онлайн

Больше событий в календаре

Другое

30 сентября

VII премия в сфере фармацевтического маркетинга и креатива MedMen Awards 2025

Онлайн

Больше событий в календаре

Маркетинг

Другое

30 октября

Премия «Проксима»

Москва

Больше событий в календаре

Маркетинг

Реклама

АО СЕЛЕКТЕЛ

ИНН 7810962785

erid: 2VSb5zDBFAH

1 000 000 стартапам

Кешбэк 30% на год в Selectel!

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr